/**
 * 
 */
package org.swing.utility.common.process;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;

import org.swing.utility.common.exception.SAMException;
import org.swing.utility.common.string.StringUtil;

/**
 * @author lqnhu
 *
 */
public class ProcessExecutor {
	private static final ExecutorService executorService = Executors
			.newCachedThreadPool(new ThreadFactory() {
				public Thread newThread(final Runnable r) {
					return new Thread(r, "ProcessExecutor Thread");
				}
			});

	/**
	 * Executes the command via Runtime.getRuntime().exec() then writes stderr
	 * to log.error and stdout to log.info and blocks until the command is
	 * complete.
	 *
	 * @see Runtime#exec(String)
	 *
	 * @param command
	 *            command string
	 * @return return code of command
	 */
	public static int execute(final String command) {
		try {
			final Process process = Runtime.getRuntime().exec(command);
			return readStreamsAndWaitFor(process);
		} catch (Throwable t) {
			throw new SAMException("Unexpected exception executing ["
					+ StringUtil.join(" ", command) + "]",
					t);
		}
	}

	/**
	 * Executes the command via Runtime.getRuntime().exec() then writes stderr
	 * to log.error and stdout to log.info and blocks until the command is
	 * complete.
	 *
	 * @see Runtime#exec(String[])
	 *
	 * @param commandParts
	 *            command string
	 * @return return code of command
	 */
	public static int execute(final String[] commandParts) {
		return execute(commandParts, null);
	}

	/**
	 * Executes the command via Runtime.getRuntime().exec(), writes
	 * <code>outputStreamString</code> to the process output stream if it is not
	 * null, then writes stderr to log.error and stdout to log.info and blocks
	 * until the command is complete.
	 *
	 * @see Runtime#exec(String[])
	 *
	 * @param commandParts
	 *            command string
	 * @return return code of command
	 */
	public static int execute(final String[] commandParts,
			String outputStreamString) {
		try {
			final Process process = Runtime.getRuntime().exec(commandParts);
			if (outputStreamString != null) {
				BufferedWriter writer = new BufferedWriter(
						new OutputStreamWriter(process.getOutputStream()));
				writer.write(outputStreamString);
				writer.newLine();
				writer.close();
			}
			return readStreamsAndWaitFor(process);
		} catch (Throwable t) {
			throw new SAMException("Unexpected exception executing ["
					+ StringUtil.join(" ", commandParts)
					+ "]", t);
		}
	}

	public static String executeAndReturnResult(final String command) {
		try {
			final Process process = Runtime.getRuntime().exec(command);
			final StringBuilderProcessOutputReader err = new StringBuilderProcessOutputReader(
					process.getErrorStream());
			final Future<?> stderrReader = executorService.submit(err);
			final StringBuilderProcessOutputReader stdout = new StringBuilderProcessOutputReader(
					process.getInputStream());
			stdout.run();
			// wait for stderr reader to be done
			stderrReader.get();
			final int result = process.waitFor();
			return result == 0 ? stdout.getOutput() : err.getOutput();
		} catch (Throwable t) {
			throw new SAMException("Unexpected exception executing [" + command
					+ "]", t);
		}
	}

	public static class ExitStatusAndOutput {
		public final int exitStatus;
		public final String stdout;
		/** May be null if interleaved */
		public final String stderr;

		public ExitStatusAndOutput(int exitStatus, String stdout, String stderr) {
			this.exitStatus = exitStatus;
			this.stdout = stdout;
			this.stderr = stderr;
		}
	}

	/**
	 * Execute the command and capture stdout and stderr.
	 * 
	 * @return Exit status of command, and both stderr and stdout interleaved
	 *         into stdout attribute.
	 */
	public static ExitStatusAndOutput executeAndReturnInterleavedOutput(
			final String command) {
		try {
			final Process process = Runtime.getRuntime().exec(command);
			return interleaveProcessOutput(process);
		} catch (Throwable t) {
			throw new SAMException("Unexpected exception executing [" + command
					+ "]", t);
		}
	}

	/**
	 * Execute the command and capture stdout and stderr.
	 * 
	 * @return Exit status of command, and both stderr and stdout interleaved
	 *         into stdout attribute.
	 */
	public static ExitStatusAndOutput executeAndReturnInterleavedOutput(
			final String[] commandArray) {
		try {
			final Process process = Runtime.getRuntime().exec(commandArray);
			return interleaveProcessOutput(process);
		} catch (Throwable t) {
			throw new SAMException("Unexpected exception executing ["
					+ StringUtil.join(" ", commandArray) + "]", t);
		}
	}

	private static ExitStatusAndOutput interleaveProcessOutput(
			final Process process) throws InterruptedException, IOException {
		final BufferedReader stdoutReader = new BufferedReader(
				new InputStreamReader(process.getInputStream()));
		final BufferedReader stderrReader = new BufferedReader(
				new InputStreamReader(process.getErrorStream()));
		final StringBuilder sb = new StringBuilder();
		String stdoutLine = null;
		String stderrLine = null;
		while ((stderrLine = stderrReader.readLine()) != null
				|| (stdoutLine = stdoutReader.readLine()) != null) {
			if (stderrLine != null)
				sb.append(stderrLine).append('\n');
			if (stdoutLine != null)
				sb.append(stdoutLine).append('\n');
			stderrLine = null;
			stdoutLine = null;
		}
		return new ExitStatusAndOutput(process.waitFor(), sb.toString(), null);
	}

	private static int readStreamsAndWaitFor(final Process process)
			throws InterruptedException, ExecutionException {
		final Future<?> stderrReader = executorService
				.submit(new LogErrorProcessOutputReader(process
						.getErrorStream()));
		new LogInfoProcessOutputReader(process.getInputStream()).run();
		// wait for stderr reader to be done
		stderrReader.get();
		return process.waitFor();
	}

	/**
	 * Runnable that reads off the given stream and logs it somewhere.
	 */
	private static abstract class ProcessOutputReader implements Runnable {
		private final BufferedReader reader;

		public ProcessOutputReader(final InputStream stream) {
			reader = new BufferedReader(new InputStreamReader(stream));
		}

		public void run() {
			try {
				String line;
				while ((line = reader.readLine()) != null) {
					write(line);
				}
			} catch (IOException e) {
				throw new SAMException(
						"Unexpected exception reading from process stream", e);
			}
		}

		protected abstract void write(String message);
	}

	private static class LogErrorProcessOutputReader extends
			ProcessOutputReader {
		public LogErrorProcessOutputReader(final InputStream stream) {
			super(stream);
		}

		@Override
		protected void write(final String message) {
			
		}
	}

	private static class LogInfoProcessOutputReader extends ProcessOutputReader {
		public LogInfoProcessOutputReader(final InputStream stream) {
			super(stream);
		}

		@Override
		protected void write(final String message) {
			
		}
	}

	private static class StringBuilderProcessOutputReader extends
			ProcessOutputReader {
		private final StringBuilder sb = new StringBuilder();

		public StringBuilderProcessOutputReader(final InputStream stream) {
			super(stream);
		}

		@Override
		protected void write(final String message) {
			sb.append(message).append("\n");
		}

		public String getOutput() {
			return sb.toString();
		}
	}
}
